﻿using System.Diagnostics;
using System.Drawing;
using System.Text;
using Microsoft.Extensions.Logging;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member

namespace TerminalGuiFluentTesting;

/// <summary>
///     Fluent API context for testing a Terminal.Gui application. Create
///     an instance using <see cref="With"/> static class.
/// </summary>
public class GuiTestContext : IDisposable
{
    private readonly CancellationTokenSource _cts = new ();
    private readonly CancellationTokenSource _hardStop;
    private readonly Task _runTask;
    private Exception? _ex;
    private readonly FakeOutput _output = new ();
    private readonly FakeWindowsInput _winInput;
    private readonly FakeNetInput _netInput;
    private View? _lastView;
    private readonly object _logsLock = new ();
    private readonly StringBuilder _logsSb;
    private readonly TestDriver _driver;
    private bool _finished;
    private readonly FakeSizeMonitor _fakeSizeMonitor;
    private readonly TimeSpan _timeout;

    internal GuiTestContext (Func<Toplevel> topLevelBuilder, int width, int height, TestDriver driver, TextWriter? logWriter = null, TimeSpan? timeout = null)
    {
        _timeout = timeout ?? TimeSpan.FromSeconds (30);
        _hardStop = new (_timeout);
        // Remove frame limit
        Application.MaximumIterationsPerSecond = ushort.MaxValue;

        IApplication origApp = ApplicationImpl.Instance;
        ILogger? origLogger = Logging.Logger;
        _logsSb = new ();
        _driver = driver;

        _netInput = new (_cts.Token);
        _winInput = new (_cts.Token);

        _output.Size = new (width, height);
        _fakeSizeMonitor = new (_output, _output.LastBuffer!);

        IComponentFactory cf = driver == TestDriver.DotNet
                                   ? new FakeNetComponentFactory (_netInput, _output, _fakeSizeMonitor)
                                   : (IComponentFactory)new FakeWindowsComponentFactory (_winInput, _output, _fakeSizeMonitor);

        var impl = new ApplicationImpl (cf);

        var booting = new SemaphoreSlim (0, 1);

        // Start the application in a background thread
        _runTask = Task.Run (
                             () =>
                             {
                                 try
                                 {
                                     ApplicationImpl.ChangeInstance (impl);

                                     ILogger logger = LoggerFactory.Create (
                                                                            builder =>
                                                                                builder.SetMinimumLevel (LogLevel.Trace)
                                                                                       .AddProvider (
                                                                                                     new TextWriterLoggerProvider (
                                                                                                      new ThreadSafeStringWriter (_logsSb, _logsLock))))
                                                                   .CreateLogger ("Test Logging");
                                     Logging.Logger = logger;

                                     impl.Init (null, GetDriverName ());

                                     booting.Release ();

                                     Toplevel t = topLevelBuilder ();
                                     t.Closed += (s, e) => { _finished = true; };
                                     Application.Run (t); // This will block, but it's on a background thread now

                                     t.Dispose ();
                                     Application.Shutdown ();
                                     _cts.Cancel ();
                                 }
                                 catch (OperationCanceledException)
                                 { }
                                 catch (Exception ex)
                                 {
                                     _ex = ex;

                                     if (logWriter != null)
                                     {
                                         WriteOutLogs (logWriter);
                                     }

                                     _hardStop.Cancel ();
                                 }
                                 finally
                                 {
                                     ApplicationImpl.ChangeInstance (origApp);
                                     Logging.Logger = origLogger;
                                     _finished = true;

                                     Application.MaximumIterationsPerSecond = Application.DefaultMaximumIterationsPerSecond;
                                 }
                             },
                             _cts.Token);

        // Wait for booting to complete with a timeout to avoid hangs
        if (!booting.WaitAsync (_timeout).Result)
        {
            throw new TimeoutException ("Application failed to start within the allotted time.");
        }

        ResizeConsole (width, height);

        if (_ex != null)
        {
            throw new ("Application crashed", _ex);
        }
    }

    private string GetDriverName ()
    {
        return _driver switch
               {
                   TestDriver.Windows => "windows",
                   TestDriver.DotNet => "dotnet",
                   _ =>
                       throw new ArgumentOutOfRangeException ()
               };
    }

    /// <summary>
    ///     Stops the application and waits for the background thread to exit.
    /// </summary>
    public GuiTestContext Stop ()
    {
        if (_runTask.IsCompleted)
        {
            return this;
        }

        WaitIteration (() => { Application.RequestStop (); });

        // Wait for the application to stop, but give it a 1-second timeout
        if (!_runTask.Wait (TimeSpan.FromMilliseconds (1000)))
        {
            _cts.Cancel ();

            // Timeout occurred, force the task to stop
            _hardStop.Cancel ();

            // App is having trouble shutting down, try sending some more shutdown stuff from this thread.
            // If this doesn't work there will be test cascade failures as the main loop continues to run during next test.
            try
            {
                Application.RequestStop ();
                Application.Shutdown ();
            }
            catch (Exception)
            {
                throw new TimeoutException ("Application failed to stop within the allotted time.", _ex);
            }

            throw new TimeoutException ("Application failed to stop within the allotted time.", _ex);
        }

        _cts.Cancel ();

        if (_ex != null)
        {
            throw _ex; // Propagate any exception that happened in the background task
        }

        return this;
    }

    /// <summary>
    ///     Hard stops the application and waits for the background thread to exit.
    /// </summary>
    public void HardStop (Exception? ex = null)
    {
        if (ex != null)
        {
            _ex = ex;
        }

        _hardStop.Cancel ();
        Stop ();
    }

    /// <summary>
    ///     Cleanup to avoid state bleed between tests
    /// </summary>
    public void Dispose ()
    {
        Stop ();

        if (_hardStop.IsCancellationRequested)
        {
            throw new (
                       "Application was hard stopped, typically this means it timed out or did not shutdown gracefully. Ensure you call Stop in your test",
                       _ex);
        }

        _hardStop.Cancel ();
    }

    /// <summary>
    ///     Adds the given <paramref name="v"/> to the current top level view
    ///     and performs layout.
    /// </summary>
    /// <param name="v"></param>
    /// <returns></returns>
    public GuiTestContext Add (View v)
    {
        WaitIteration (
                       () =>
                       {
                           Toplevel top = Application.Top ?? throw new ("Top was null so could not add view");
                           top.Add (v);
                           top.Layout ();
                           _lastView = v;
                       });

        return this;
    }

    /// <summary>
    ///     Simulates changing the console size e.g. by resizing window in your operating system
    /// </summary>
    /// <param name="width">new Width for the console.</param>
    /// <param name="height">new Height for the console.</param>
    /// <returns></returns>
    public GuiTestContext ResizeConsole (int width, int height)
    {
        return WaitIteration (
                              () =>
                              {
                                  Application.Driver!.SetScreenSize(width, height);
                              });
    }

    public GuiTestContext ScreenShot (string title, TextWriter writer)
    {
        return WaitIteration (
                              () =>
                              {
                                  writer.WriteLine (title + ":");
                                  var text = Application.ToString ();

                                  writer.WriteLine (text);
                              });
    }

    /// <summary>
    ///     Writes all Terminal.Gui engine logs collected so far to the <paramref name="writer"/>
    /// </summary>
    /// <param name="writer"></param>
    /// <returns></returns>
    public GuiTestContext WriteOutLogs (TextWriter writer)
    {
        lock (_logsLock)
        {
            writer.WriteLine (_logsSb.ToString ());
        }

        return this; //WaitIteration();
    }

    /// <summary>
    ///     Waits until the end of the current iteration of the main loop. Optionally
    ///     running a given <paramref name="a"/> action on the UI thread at that time.
    /// </summary>
    /// <param name="a"></param>
    /// <returns></returns>
    public GuiTestContext WaitIteration (Action? a = null)
    {
        // If application has already exited don't wait!
        if (_finished || _cts.Token.IsCancellationRequested || _hardStop.Token.IsCancellationRequested)
        {
            return this;
        }

        if (Thread.CurrentThread.ManagedThreadId == Application.MainThreadId)
        {
            throw new NotSupportedException ("Cannot WaitIteration during Invoke");
        }

        a ??= () => { };
        var ctsLocal = new CancellationTokenSource ();

        Application.Invoke (
                            () =>
                            {
                                try
                                {
                                    a ();
                                    ctsLocal.Cancel ();
                                }
                                catch (Exception e)
                                {
                                    _ex = e;
                                    _hardStop.Cancel ();
                                }
                            });

        // Blocks until either the token or the hardStopToken is cancelled.
        WaitHandle.WaitAny (
                            new []
                            {
                                _cts.Token.WaitHandle,
                                _hardStop.Token.WaitHandle,
                                ctsLocal.Token.WaitHandle
                            });

        return this;
    }

    /// <summary>
    ///     Performs the supplied <paramref name="doAction"/> immediately.
    ///     Enables running commands without breaking the Fluent API calls.
    /// </summary>
    /// <param name="doAction"></param>
    /// <returns></returns>
    public GuiTestContext Then (Action doAction)
    {
        try
        {
            WaitIteration (doAction);
        }
        catch (Exception ex)
        {
            _ex = ex;
            HardStop ();

            throw;
        }

        return this;
    }

    /// <summary>
    ///     Simulates a right click at the given screen coordinates on the current driver.
    ///     This is a raw input event that goes through entire processing pipeline as though
    ///     user had pressed the mouse button physically.
    /// </summary>
    /// <param name="screenX">0 indexed screen coordinates</param>
    /// <param name="screenY">0 indexed screen coordinates</param>
    /// <returns></returns>
    public GuiTestContext RightClick (int screenX, int screenY) { return Click (WindowsConsole.ButtonState.Button3Pressed, screenX, screenY); }

    /// <summary>
    ///     Simulates a left click at the given screen coordinates on the current driver.
    ///     This is a raw input event that goes through entire processing pipeline as though
    ///     user had pressed the mouse button physically.
    /// </summary>
    /// <param name="screenX">0 indexed screen coordinates</param>
    /// <param name="screenY">0 indexed screen coordinates</param>
    /// <returns></returns>
    public GuiTestContext LeftClick (int screenX, int screenY) { return Click (WindowsConsole.ButtonState.Button1Pressed, screenX, screenY); }

    public GuiTestContext LeftClick<T> (Func<T, bool> evaluator) where T : View { return Click (WindowsConsole.ButtonState.Button1Pressed, evaluator); }

    private GuiTestContext Click<T> (WindowsConsole.ButtonState btn, Func<T, bool> evaluator) where T : View
    {
        T v;
        var screen = Point.Empty;

        GuiTestContext ctx = WaitIteration (
                                            () =>
                                            {
                                                v = Find (evaluator);
                                                screen = v.ViewportToScreen (new Point (0, 0));
                                            });

        Click (btn, screen.X, screen.Y);

        return ctx;
    }

    private GuiTestContext Click (WindowsConsole.ButtonState btn, int screenX, int screenY)
    {
        switch (_driver)
        {
            case TestDriver.Windows:

                _winInput.InputBuffer!.Enqueue (
                                                new ()
                                                {
                                                    EventType = WindowsConsole.EventType.Mouse,
                                                    MouseEvent = new ()
                                                    {
                                                        ButtonState = btn,
                                                        MousePosition = new ((short)screenX, (short)screenY)
                                                    }
                                                });

                _winInput.InputBuffer.Enqueue (
                                               new ()
                                               {
                                                   EventType = WindowsConsole.EventType.Mouse,
                                                   MouseEvent = new ()
                                                   {
                                                       ButtonState = WindowsConsole.ButtonState.NoButtonPressed,
                                                       MousePosition = new ((short)screenX, (short)screenY)
                                                   }
                                               });

                return WaitUntil (() => _winInput.InputBuffer.IsEmpty);

            case TestDriver.DotNet:

                int netButton = btn switch
                                {
                                    WindowsConsole.ButtonState.Button1Pressed => 0,
                                    WindowsConsole.ButtonState.Button2Pressed => 1,
                                    WindowsConsole.ButtonState.Button3Pressed => 2,
                                    WindowsConsole.ButtonState.RightmostButtonPressed => 2,
                                    _ => throw new ArgumentOutOfRangeException (nameof (btn))
                                };

                foreach (ConsoleKeyInfo k in NetSequences.Click (netButton, screenX, screenY))
                {
                    SendNetKey (k, false);
                }

                return WaitIteration ();
            default:
                throw new ArgumentOutOfRangeException ();
        }
    }

    private GuiTestContext WaitUntil (Func<bool> condition)
    {
        GuiTestContext? c = null;
        var sw = Stopwatch.StartNew ();

        while (!condition ())
        {
            if (sw.Elapsed > _timeout)
            {
                throw new TimeoutException ("Failed to reach condition within the time limit");
            }

            c = WaitIteration ();
        }

        return c ?? this;
    }

    public GuiTestContext Down ()
    {
        switch (_driver)
        {
            case TestDriver.Windows:
                SendWindowsKey (ConsoleKeyMapping.VK.DOWN);

                break;
            case TestDriver.DotNet:
                foreach (ConsoleKeyInfo k in NetSequences.Down)
                {
                    SendNetKey (k);
                }

                break;
            default:
                throw new ArgumentOutOfRangeException ();
        }

        return WaitIteration ();
    }

    /// <summary>
    ///     Simulates the Right cursor key
    /// </summary>
    /// <returns></returns>
    /// <exception cref="ArgumentOutOfRangeException"></exception>
    public GuiTestContext Right ()
    {
        switch (_driver)
        {
            case TestDriver.Windows:
                SendWindowsKey (ConsoleKeyMapping.VK.RIGHT);

                break;
            case TestDriver.DotNet:
                foreach (ConsoleKeyInfo k in NetSequences.Right)
                {
                    SendNetKey (k);
                }

                WaitIteration ();

                break;
            default:
                throw new ArgumentOutOfRangeException ();
        }

        return WaitIteration ();
    }

    /// <summary>
    ///     Simulates the Left cursor key
    /// </summary>
    /// <returns></returns>
    /// <exception cref="ArgumentOutOfRangeException"></exception>
    public GuiTestContext Left ()
    {
        switch (_driver)
        {
            case TestDriver.Windows:
                SendWindowsKey (ConsoleKeyMapping.VK.LEFT);

                break;
            case TestDriver.DotNet:
                foreach (ConsoleKeyInfo k in NetSequences.Left)
                {
                    SendNetKey (k);
                }

                break;
            default:
                throw new ArgumentOutOfRangeException ();
        }

        return WaitIteration ();
    }

    /// <summary>
    ///     Simulates the up cursor key
    /// </summary>
    /// <returns></returns>
    /// <exception cref="ArgumentOutOfRangeException"></exception>
    public GuiTestContext Up ()
    {
        switch (_driver)
        {
            case TestDriver.Windows:
                SendWindowsKey (ConsoleKeyMapping.VK.UP);

                break;
            case TestDriver.DotNet:
                foreach (ConsoleKeyInfo k in NetSequences.Up)
                {
                    SendNetKey (k);
                }

                break;
            default:
                throw new ArgumentOutOfRangeException ();
        }

        return WaitIteration ();
    }

    /// <summary>
    ///     Simulates pressing the Return/Enter (newline) key.
    /// </summary>
    /// <returns></returns>
    /// <exception cref="ArgumentOutOfRangeException"></exception>
    public GuiTestContext Enter ()
    {
        switch (_driver)
        {
            case TestDriver.Windows:
                SendWindowsKey (
                                new WindowsConsole.KeyEventRecord
                                {
                                    UnicodeChar = '\r',
                                    dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed,
                                    wRepeatCount = 1,
                                    wVirtualKeyCode = ConsoleKeyMapping.VK.RETURN,
                                    wVirtualScanCode = 28
                                });

                break;
            case TestDriver.DotNet:
                SendNetKey (new ('\r', ConsoleKey.Enter, false, false, false));

                break;
            default:
                throw new ArgumentOutOfRangeException ();
        }

        return WaitIteration ();
    }

    /// <summary>
    ///     Simulates pressing the Esc (Escape) key.
    /// </summary>
    /// <returns></returns>
    /// <exception cref="ArgumentOutOfRangeException"></exception>
    public GuiTestContext Escape ()
    {
        switch (_driver)
        {
            case TestDriver.Windows:
                SendWindowsKey (
                                new WindowsConsole.KeyEventRecord
                                {
                                    UnicodeChar = '\u001b',
                                    dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed,
                                    wRepeatCount = 1,
                                    wVirtualKeyCode = ConsoleKeyMapping.VK.ESCAPE,
                                    wVirtualScanCode = 1
                                });

                break;
            case TestDriver.DotNet:

                // Note that this accurately describes how Esc comes in. Typically, ConsoleKey is None
                // even though you would think it would be Escape - it isn't
                SendNetKey (new ('\u001b', ConsoleKey.None, false, false, false));

                break;
            default:
                throw new ArgumentOutOfRangeException ();
        }

        return this;
    }

    /// <summary>
    ///     Simulates pressing the Tab key.
    /// </summary>
    /// <returns></returns>
    /// <exception cref="ArgumentOutOfRangeException"></exception>
    public GuiTestContext Tab ()
    {
        switch (_driver)
        {
            case TestDriver.Windows:
                SendWindowsKey (
                                new WindowsConsole.KeyEventRecord
                                {
                                    UnicodeChar = '\t',
                                    dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed,
                                    wRepeatCount = 1,
                                    wVirtualKeyCode = 0,
                                    wVirtualScanCode = 0
                                });

                break;
            case TestDriver.DotNet:

                // Note that this accurately describes how Tab comes in. Typically, ConsoleKey is None
                // even though you would think it would be Tab - it isn't
                SendNetKey (new ('\t', ConsoleKey.None, false, false, false));

                break;
            default:
                throw new ArgumentOutOfRangeException ();
        }

        return this;
    }

    /// <summary>
    ///     Registers a right click handler on the <see cref="LastView"/> added view (or root view) that
    ///     will open the supplied <paramref name="contextMenu"/>.
    /// </summary>
    /// <param name="contextMenu"></param>
    /// <returns></returns>
    public GuiTestContext WithContextMenu (PopoverMenu? contextMenu)
    {
        LastView.MouseEvent += (s, e) =>
                               {
                                   if (e.Flags.HasFlag (MouseFlags.Button3Clicked))
                                   {
                                       // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused
                                       // and the context menu is disposed when it is closed.
                                       Application.Popover?.Register (contextMenu);
                                       contextMenu?.MakeVisible (e.ScreenPosition);
                                   }
                               };

        return this;
    }

    /// <summary>
    ///     The last view added (e.g. with <see cref="Add"/>) or the root/current top.
    /// </summary>
    public View LastView => _lastView ?? Application.Top ?? throw new ("Could not determine which view to add to");

    /// <summary>
    ///     Send a full windows OS key including both down and up.
    /// </summary>
    /// <param name="fullKey"></param>
    private void SendWindowsKey (WindowsConsole.KeyEventRecord fullKey)
    {
        WindowsConsole.KeyEventRecord down = fullKey;
        WindowsConsole.KeyEventRecord up = fullKey; // because struct this is new copy

        down.bKeyDown = true;
        up.bKeyDown = false;

        _winInput.InputBuffer!.Enqueue (
                                        new ()
                                        {
                                            EventType = WindowsConsole.EventType.Key,
                                            KeyEvent = down
                                        });

        _winInput.InputBuffer.Enqueue (
                                       new ()
                                       {
                                           EventType = WindowsConsole.EventType.Key,
                                           KeyEvent = up
                                       });

        WaitIteration ();
    }

    private void SendNetKey (ConsoleKeyInfo consoleKeyInfo, bool wait = true)
    {
        _netInput.InputBuffer!.Enqueue (consoleKeyInfo);

        if (wait)
        {
            WaitUntil (() => _netInput.InputBuffer.IsEmpty);
        }
    }

    /// <summary>
    ///     Sends a special key e.g. cursor key that does not map to a specific character
    /// </summary>
    /// <param name="specialKey"></param>
    private void SendWindowsKey (ConsoleKeyMapping.VK specialKey)
    {
        _winInput.InputBuffer!.Enqueue (
                                        new ()
                                        {
                                            EventType = WindowsConsole.EventType.Key,
                                            KeyEvent = new ()
                                            {
                                                bKeyDown = true,
                                                wRepeatCount = 0,
                                                wVirtualKeyCode = specialKey,
                                                wVirtualScanCode = 0,
                                                UnicodeChar = '\0',
                                                dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed
                                            }
                                        });

        _winInput.InputBuffer.Enqueue (
                                       new ()
                                       {
                                           EventType = WindowsConsole.EventType.Key,
                                           KeyEvent = new ()
                                           {
                                               bKeyDown = false,
                                               wRepeatCount = 0,
                                               wVirtualKeyCode = specialKey,
                                               wVirtualScanCode = 0,
                                               UnicodeChar = '\0',
                                               dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed
                                           }
                                       });

        WaitIteration ();
    }

    /// <summary>
    ///     Sends a key to the application. This goes directly to Application and does not go through
    ///     a driver.
    /// </summary>
    /// <param name="key"></param>
    /// <returns></returns>
    public GuiTestContext RaiseKeyDownEvent (Key key)
    {
        WaitIteration (() => Application.RaiseKeyDownEvent (key));

        return this; //WaitIteration();
    }

    /// <summary>
    ///     Sets the input focus to the given <see cref="View"/>.
    ///     Throws <see cref="ArgumentException"/> if focus did not change due to system
    ///     constraints e.g. <paramref name="toFocus"/>
    ///     <see cref="View.CanFocus"/> is <see langword="false"/>
    /// </summary>
    /// <param name="toFocus"></param>
    /// <returns></returns>
    /// <exception cref="ArgumentException"></exception>
    public GuiTestContext Focus (View toFocus)
    {
        toFocus.FocusDeepest (NavigationDirection.Forward, TabBehavior.TabStop);

        if (!toFocus.HasFocus)
        {
            throw new ArgumentException ("Failed to set focus, FocusDeepest did not result in HasFocus becoming true. Ensure view is added and focusable");
        }

        return WaitIteration ();
    }

    /// <summary>
    ///     Tabs through the UI until a View matching the <paramref name="evaluator"/>
    ///     is found (of Type T) or all views are looped through (back to the beginning)
    ///     in which case triggers hard stop and Exception
    /// </summary>
    /// <param name="evaluator">
    ///     Delegate that returns true if the passed View is the one
    ///     you are trying to focus. Leave <see langword="null"/> to focus the first view of type
    ///     <typeparamref name="T"/>
    /// </param>
    /// <returns></returns>
    /// <exception cref="ArgumentException"></exception>
    public GuiTestContext Focus<T> (Func<T, bool>? evaluator = null) where T : View
    {
        evaluator ??= _ => true;
        Toplevel? t = Application.Top;

        HashSet<View> seen = new ();

        if (t == null)
        {
            Fail ("Application.Top was null when trying to set focus");

            return this;
        }

        do
        {
            View? next = t.MostFocused;

            // Is view found?
            if (next is T v && evaluator (v))
            {
                return this;
            }

            // No, try tab to the next (or first)
            Tab ();
            WaitIteration ();

            next = t.MostFocused;

            if (next is null)
            {
                Fail (
                      "Failed to tab to a view which matched the Type and evaluator constraints of the test because MostFocused became or was always null"
                      + DescribeSeenViews (seen));

                return this;
            }

            // Track the views we have seen
            // We have looped around to the start again if it was already there
            if (!seen.Add (next))
            {
                Fail (
                      "Failed to tab to a view which matched the Type and evaluator constraints of the test before looping back to the original View"
                      + DescribeSeenViews (seen));

                return this;
            }
        }
        while (true);
    }

    private string DescribeSeenViews (HashSet<View> seen) { return Environment.NewLine + string.Join (Environment.NewLine, seen); }

    private T Find<T> (Func<T, bool> evaluator) where T : View
    {
        Toplevel? t = Application.Top;

        if (t == null)
        {
            Fail ("Application.Top was null when attempting to find view");
        }

        T? f = FindRecursive (t!, evaluator);

        if (f == null)
        {
            Fail ("Failed to tab to a view which matched the Type and evaluator constraints in any SubViews of top");
        }

        return f!;
    }

    private T? FindRecursive<T> (View current, Func<T, bool> evaluator) where T : View
    {
        foreach (View subview in current.SubViews)
        {
            if (subview is T match && evaluator (match))
            {
                return match;
            }

            // Recursive call
            T? result = FindRecursive (subview, evaluator);

            if (result != null)
            {
                return result;
            }
        }

        return null;
    }

    private void Fail (string reason)
    {
        Stop ();

        throw new (reason);
    }

    public GuiTestContext Send (Key key)
    {
        return WaitIteration (
                              () =>
                              {
                                  if (Application.Driver is IConsoleDriverFacade facade)
                                  {
                                      facade.InputProcessor.OnKeyDown (key);
                                      facade.InputProcessor.OnKeyUp (key);
                                  }
                                  else
                                  {
                                      Fail ("Expected Application.Driver to be IConsoleDriverFacade");
                                  }
                              });
    }

    /// <summary>
    ///     Returns the last set position of the cursor.
    /// </summary>
    /// <returns></returns>
    public Point GetCursorPosition () { return _output.CursorPosition; }
}

internal class FakeWindowsComponentFactory (FakeWindowsInput winInput, FakeOutput output, FakeSizeMonitor fakeSizeMonitor)
    : WindowsComponentFactory
{
    /// <inheritdoc/>
    public override IConsoleInput<WindowsConsole.InputRecord> CreateInput () { return winInput; }

    /// <inheritdoc/>
    public override IConsoleOutput CreateOutput () { return output; }

    /// <inheritdoc/>
    public override IConsoleSizeMonitor CreateConsoleSizeMonitor (IConsoleOutput consoleOutput, IOutputBuffer outputBuffer)
    {
        outputBuffer.SetSize (consoleOutput.GetSize ().Width, consoleOutput.GetSize ().Height);
        return fakeSizeMonitor;
    }
}

internal class FakeNetComponentFactory (FakeNetInput netInput, FakeOutput output, FakeSizeMonitor fakeSizeMonitor) : NetComponentFactory
{
    /// <inheritdoc/>
    public override IConsoleInput<ConsoleKeyInfo> CreateInput () { return netInput; }

    /// <inheritdoc/>
    public override IConsoleOutput CreateOutput () { return output; }

    /// <inheritdoc/>
    public override IConsoleSizeMonitor CreateConsoleSizeMonitor (IConsoleOutput consoleOutput, IOutputBuffer outputBuffer)
    {
        outputBuffer.SetSize (consoleOutput.GetSize ().Width, consoleOutput.GetSize ().Height);
        return fakeSizeMonitor;
    }
}
